/*
 * Copyright (C) 2016 venshine.cn@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.wx.beziermaker;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.multimodalinput.event.TouchEvent;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.ArrayList;


/**
 * 贝塞尔曲线
 *
 * @author venshine
 */
public class BezierView extends Component
        implements Component.DrawTask, Component.LayoutRefreshedListener, Component.TouchEventListener {
    // 贝塞尔曲线最大阶数
    private static final int MAX_COUNT = 7;

    // 合法区域宽度
    private static final int REGION_WIDTH = 30;

    // 矩形尺寸
    private static final int FINGER_RECT_SIZE = 60;

    // 贝塞尔曲线线宽
    private static final int BEZIER_WIDTH = 10;

    // 切线线宽
    private static final int TANGENT_WIDTH = 6;

    // 控制点连线线宽
    private static final int CONTROL_WIDTH = 12;

    // 控制点半径
    private static final int CONTROL_RADIUS = 12;

    // 文字画笔尺寸
    private static final int TEXT_SIZE = 40;

    // 文本高度
    private static final int TEXT_HEIGHT = 60;

    // 移动速率
    private static final int RATE = 10;
    private static final int HANDLER_WHAT = 100;

    // 1000帧
    private static final int FRAME = 1000;

    // 切线颜色
    private static final String[] TANGENT_COLORS = {"#7fff00", "#7a67ee", "#ee82ee", "#ffd700", "#1c86ee", "#8b8b00"};
    private static final int STATE_READY = 0x0001;
    private static final int STATE_RUNNING = 0x0002;
    private static final int STATE_STOP = 0x0004;
    private static final int STATE_TOUCH = 0x0010;

    private static final String[] ORDER_STRS = {"一", "二", "三", "四", "五", "六", "七"};

    // 贝塞尔曲线路径
    private Path mBezierPath = null;

    // 贝塞尔曲线画笔
    private Paint mBezierPaint = null;

    // 移动点画笔
    private Paint mMovingPaint = null;

    // 控制点画笔
    private Paint mControlPaint = null;

    // 切线画笔
    private Paint mTangentPaint = null;

    // 固定线画笔
    private Paint mLinePaint = null;

    // 点画笔
    private Paint mTextPointPaint = null;

    // 文字画笔
    private Paint mTextPaint = null;

    // 贝塞尔曲线点集
    private ArrayList<Point> mBezierPoints = null;

    // 贝塞尔曲线移动点
    private Point mBezierPoint = null;

    // 控制点集
    private ArrayList<Point> mControlPoints = null;

    // 切线点集
    private ArrayList<ArrayList<ArrayList<Point>>> mTangentPoints;
    private ArrayList<ArrayList<Point>> mInstantTangentPoints;

    // 移动速率
    private int mR = 0;

    // 速率
    private int mRate = RATE;

    // 状态
    private int mState;

    // 设置是否循环
    private boolean mLoop = false;

    // 设置是否显示切线
    private boolean mTangent = true;

    // 画布宽
    private int mWidth = 0;

    // 画布高
    private int mHeight = 0;

    // 当前移动的控制点
    private Point mCurPoint;

    private float[][] mRegion = null;

    private EventHandler mHandler =
            new EventHandler(EventRunner.getMainEventRunner()) {
                @Override
                protected void processEvent(InnerEvent event) {
                    super.processEvent(event);
                    if (event.eventId == HANDLER_WHAT) {
                        mR += mRate;
                        if (mR >= mBezierPoints.size()) {
                            removeEvent(HANDLER_WHAT);
                            mR = 0;
                            mState &= ~STATE_RUNNING;
                            mState &= ~STATE_STOP;
                            mState |= STATE_READY | STATE_TOUCH;
                            if (mLoop) {
                                start();
                            }
                            return;
                        }
                        if (mR != mBezierPoints.size() - 1 && mR + mRate >= mBezierPoints.size()) {
                            mR = mBezierPoints.size() - 1;
                        }

                        // Bezier点
                        mBezierPoint = new Point(mBezierPoints.get(mR).getPointX(), mBezierPoints.get(mR).getPointY());

                        // 切线点
                        if (mTangent) {
                            int size = mTangentPoints.size();
                            ArrayList<Point> instantpoints;
                            mInstantTangentPoints = new ArrayList<>();
                            for (int i = 0; i < size; i++) {
                                int len = mTangentPoints.get(i).size();
                                instantpoints = new ArrayList<>();
                                for (int j = 0; j < len; j++) {
                                    float x = mTangentPoints.get(i).get(j).get(mR).getPointX();
                                    float y = mTangentPoints.get(i).get(j).get(mR).getPointY();
                                    instantpoints.add(new Point(x, y));
                                }
                                mInstantTangentPoints.add(instantpoints);
                            }
                        }
                        if (mR == mBezierPoints.size() - 1) {
                            mState |= STATE_STOP;
                        }
                        invalidate();
                    }
                }
            };

    public BezierView(Context context) {
        super(context);
        init();
    }

    public BezierView(Context context, AttrSet attrSet) {
        super(context, attrSet);
        init();
    }

    public BezierView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init();
    }

    public BezierView(Context context, AttrSet attrSet, int resId) {
        super(context, attrSet, resId);
        init();
    }

    private void init() {
        // 初始坐标
        mControlPoints = new ArrayList<>(MAX_COUNT + 1);
        int screenWidth =
                getResourceManager().getDeviceCapability().width
                        * getResourceManager().getDeviceCapability().screenDensity
                        / 160;
        mControlPoints.add(new Point(screenWidth / 5, screenWidth / 5));
        mControlPoints.add(new Point(screenWidth / 3, screenWidth / 2));
        mControlPoints.add(new Point(screenWidth / 3 * 2, screenWidth / 4));
        // 贝塞尔曲线画笔
        mBezierPaint = new Paint();
        mBezierPaint.setColor(Color.RED);
        mBezierPaint.setStrokeWidth(BEZIER_WIDTH);
        mBezierPaint.setStyle(Paint.Style.STROKE_STYLE);
        mBezierPaint.setAntiAlias(true);
        // 移动点画笔
        mMovingPaint = new Paint();
        mMovingPaint.setColor(Color.BLACK);
        mMovingPaint.setAntiAlias(true);
        mMovingPaint.setStyle(Paint.Style.FILL_STYLE);
        // 控制点画笔
        mControlPaint = new Paint();
        mControlPaint.setColor(Color.BLACK);
        mControlPaint.setAntiAlias(true);
        mControlPaint.setStyle(Paint.Style.STROKE_STYLE);
        // 切线画笔
        mTangentPaint = new Paint();
        mTangentPaint.setColor(new Color(Color.getIntColor(TANGENT_COLORS[0])));
        mTangentPaint.setAntiAlias(true);
        mTangentPaint.setStrokeWidth(TANGENT_WIDTH);
        mTangentPaint.setStyle(Paint.Style.FILL_STYLE);
        // 固定线画笔
        mLinePaint = new Paint();
        mLinePaint.setColor(Color.LTGRAY);
        mLinePaint.setStrokeWidth(CONTROL_WIDTH);
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStyle(Paint.Style.FILL_STYLE);
        // 点画笔
        mTextPointPaint = new Paint();
        mTextPointPaint.setColor(Color.BLACK);
        mTextPointPaint.setAntiAlias(true);
        mTextPointPaint.setTextSize(TEXT_SIZE);
        // 文字画笔
        mTextPaint = new Paint();
        mTextPaint.setColor(Color.GRAY);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(TEXT_SIZE);
        mBezierPath = new Path();
        mState |= STATE_READY | STATE_TOUCH;
        addDrawTask(this);
        setLayoutRefreshedListener(this);
        setTouchEventListener(this);
    }

    // 创建Bezier点集
    private ArrayList<Point> buildBezierPoints() {
        ArrayList<Point> points = new ArrayList<>();
        int order = mControlPoints.size() - 1;
        float delta = 1.0f / FRAME;
        for (float t = 0; t <= 1; t += delta) {
            // Bezier点集
            points.add(new Point(deCasteljauX(order, 0, t), deCasteljauY(order, 0, t)));
        }
        return points;
    }

    // 创建切线点集
    private ArrayList<ArrayList<ArrayList<Point>>> buildTangentPoints() {
        ArrayList<Point> points; // 1条线点集
        ArrayList<ArrayList<Point>> morepoints; // 多条线点集
        ArrayList<ArrayList<ArrayList<Point>>> allpoints = new ArrayList<>(); // 所有点集
        Point point;
        int order = mControlPoints.size() - 1;
        float delta = 1.0f / FRAME;
        for (int i = 0; i < order - 1; i++) {
            int size = allpoints.size();
            morepoints = new ArrayList<>();
            for (int j = 0; j < order - i; j++) {
                points = new ArrayList<>();
                for (float t = 0; t <= 1; t += delta) {
                    float p0x;
                    float p1x;
                    float p0y;
                    float p1y;
                    int z = (int) (t * FRAME);
                    if (size > 0) {
                        p0x = allpoints.get(i - 1).get(j).get(z).getPointX();
                        p1x = allpoints.get(i - 1).get(j + 1).get(z).getPointX();
                        p0y = allpoints.get(i - 1).get(j).get(z).getPointY();
                        p1y = allpoints.get(i - 1).get(j + 1).get(z).getPointY();
                    } else {
                        p0x = mControlPoints.get(j).getPointX();
                        p1x = mControlPoints.get(j + 1).getPointX();
                        p0y = mControlPoints.get(j).getPointY();
                        p1y = mControlPoints.get(j + 1).getPointY();
                    }
                    float x = (1 - t) * p0x + t * p1x;
                    float y = (1 - t) * p0y + t * p1y;
                    point = new Point(x, y);
                    points.add(point);
                }
                morepoints.add(points);
            }
            allpoints.add(morepoints);
        }
        return allpoints;
    }

    /**
     * deCasteljau算法
     *
     * @param i 阶数
     * @param j 点
     * @param t 时间
     * @return 返回x坐标
     */
    private float deCasteljauX(int i, int j, float t) {
        if (i == 1) {
            return (1 - t) * mControlPoints.get(j).getPointX() + t * mControlPoints.get(j + 1).getPointX();
        }
        return (1 - t) * deCasteljauX(i - 1, j, t) + t * deCasteljauX(i - 1, j + 1, t);
    }

    /**
     * deCasteljau算法
     *
     * @param i 阶数
     * @param j 点
     * @param t 时间
     * @return 返回Y坐标
     */
    private float deCasteljauY(int i, int j, float t) {
        if (i == 1) {
            return (1 - t) * mControlPoints.get(j).getPointY() + t * mControlPoints.get(j + 1).getPointY();
        }
        return (1 - t) * deCasteljauY(i - 1, j, t) + t * deCasteljauY(i - 1, j + 1, t);
    }

    // 判断坐标是否在合法区域中
    private boolean isLegalTouchRegion(float x, float y) {
        if (x <= REGION_WIDTH || x >= mWidth - REGION_WIDTH || y <= REGION_WIDTH || y >= mHeight - REGION_WIDTH) {
            return false;
        }
        RectFloat rectF = new RectFloat();
        for (Point point : mControlPoints) {
            if (mCurPoint != null && mCurPoint.equals(point)) { // 判断是否是当前控制点
                continue;
            }
            rectF.left = point.getPointX() - REGION_WIDTH;
            rectF.top = point.getPointY() - REGION_WIDTH;
            rectF.right = point.getPointX() + REGION_WIDTH;
            rectF.bottom = point.getPointY() + REGION_WIDTH;
            if (rectF.isInclude(x, y)) {
                return false;
            }
        }
        return true;
    }

    // 获取合法控制点
    private Point getLegalControlPoint(float x, float y) {
        RectFloat rectF = new RectFloat();
        for (Point point : mControlPoints) {
            rectF.left = point.getPointX() - REGION_WIDTH;
            rectF.top = point.getPointY() - REGION_WIDTH;
            rectF.right = point.getPointX() + REGION_WIDTH;
            rectF.bottom = point.getPointY() + REGION_WIDTH;
            if (rectF.isInclude(x, y)) {
                return point;
            }
        }
        return null;
    }

    // 判断手指坐标是否在合法区域中
    private boolean isLegalFingerRegion(float x, float y) {
        if (mCurPoint != null) {
            RectFloat rectF =
                    new RectFloat(
                            mCurPoint.getPointX() - (FINGER_RECT_SIZE >> 1),
                            mCurPoint.getPointY() - (FINGER_RECT_SIZE >> 1),
                            mCurPoint.getPointX() + (FINGER_RECT_SIZE >> 1),
                            mCurPoint.getPointY() + (FINGER_RECT_SIZE >> 1));
            return rectF.isInclude(x, y);
        }
        return false;
    }

    @Override
    public void onRefreshed(Component component) {
        if (mWidth == 0 || mHeight == 0) {
            mWidth = getWidth();
            mHeight = getHeight();
        }
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (isRunning() && !isTouchable()) {
            if (mBezierPoint == null) {
                mBezierPath.reset();
                mBezierPoint = mBezierPoints.get(0);
                mBezierPath.moveTo(mBezierPoint.getPointX(), mBezierPoint.getPointY());
            }
            // 控制点和控制点连线
            drawLineAndPoint(canvas);
            // 切线
            drawTangent(canvas);
            // Bezier曲线
            mBezierPath.lineTo(mBezierPoint.getPointX(), mBezierPoint.getPointY());
            canvas.drawPath(mBezierPath, mBezierPaint);
            // Bezier曲线起始移动点
            canvas.drawCircle(mBezierPoint.getPointX(), mBezierPoint.getPointY(), CONTROL_RADIUS, mMovingPaint);
            // 时间展示
            canvas.drawText(
                    mTextPaint,
                    "t:" + (new DecimalFormat("##0.000").format((float) mR / FRAME)),
                    mWidth - TEXT_HEIGHT * 3,
                    mHeight - TEXT_HEIGHT);

            mHandler.removeEvent(HANDLER_WHAT);
            mHandler.sendEvent(HANDLER_WHAT);
        }
        if (isTouchable()) {
            // 控制点和控制点连线
            drawLineAndPoint(canvas);
        }
    }

    private void drawLineAndPoint(Canvas canvas) {
        int size = mControlPoints.size();
        Point point;
        for (int i = 0; i < size; i++) {
            point = mControlPoints.get(i);
            if (i > 0) {
                // 控制点连线
                canvas.drawLine(
                        new Point(mControlPoints.get(i - 1).getPointX(), mControlPoints.get(i - 1).getPointY()),
                        new Point(point.getPointX(), point.getPointY()),
                        mLinePaint);
            }
            // 控制点
            canvas.drawCircle(point.getPointX(), point.getPointY(), CONTROL_RADIUS, mControlPaint);
            // 控制点文本
            canvas.drawText(
                    mTextPointPaint,
                    "p" + i,
                    point.getPointX() + CONTROL_RADIUS * 2,
                    point.getPointY() + CONTROL_RADIUS * 2);
            // 控制点文本展示
            canvas.drawText(
                    mTextPaint,
                    "p"
                            + i
                            + " ( "
                            + new DecimalFormat("##0.0").format(point.getPointX())
                            + " "
                            + ", "
                            + new DecimalFormat("##0.0").format(point.getPointY())
                            + ") ",
                    REGION_WIDTH,
                    mHeight - (size - i) * TEXT_HEIGHT);
        }
    }

    private void drawTangent(Canvas canvas) {
        if (mTangent && mInstantTangentPoints != null && !isStop()) {
            int tsize = mInstantTangentPoints.size();
            ArrayList<Point> tps;
            for (int i = 0; i < tsize; i++) {
                tps = mInstantTangentPoints.get(i);
                int tlen = tps.size();
                for (int j = 0; j < tlen - 1; j++) {
                    mTangentPaint.setColor(new Color(Color.getIntColor(TANGENT_COLORS[i])));
                    canvas.drawLine(
                            new Point(tps.get(j).getPointX(), tps.get(j).getPointY()),
                            new Point(tps.get(j + 1).getPointX(), tps.get(j + 1).getPointY()),
                            mTangentPaint);
                    canvas.drawCircle(tps.get(j).getPointX(), tps.get(j).getPointY(), CONTROL_RADIUS, mTangentPaint);
                    canvas.drawCircle(
                            tps.get(j + 1).getPointX(), tps.get(j + 1).getPointY(), CONTROL_RADIUS, mTangentPaint);
                }
            }
        }
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (!isTouchable()) {
            return true;
        }
        switch (touchEvent.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                mState &= ~STATE_READY;
                break;
            case TouchEvent.POINT_MOVE:
                float x = EventUtil.getXInComponent(component, touchEvent);
                float y = EventUtil.getYIntComponent(component, touchEvent);
                if (mCurPoint == null) {
                    mCurPoint = getLegalControlPoint(x, y);
                }
                if (mCurPoint != null && isLegalTouchRegion(x, y)) { // 判断手指移动区域是否合法
                    if (isLegalFingerRegion(x, y)) { // 判断手指触摸区域是否合法
                        mCurPoint.modify(x, y);
                        invalidate();
                    }
                }

                break;
            case TouchEvent.PRIMARY_POINT_UP:
                mCurPoint = null;
                mState |= STATE_READY;
                break;
        }
        return true;
    }

    private boolean isReady() {
        return (mState & STATE_READY) == STATE_READY;
    }

    private boolean isRunning() {
        return (mState & STATE_RUNNING) == STATE_RUNNING;
    }

    private boolean isTouchable() {
        return (mState & STATE_TOUCH) == STATE_TOUCH;
    }

    private boolean isStop() {
        return (mState & STATE_STOP) == STATE_STOP;
    }

    /**
     * 开始
     */
    public void start() {
        if (isReady()) {
            mBezierPoint = null;
            mInstantTangentPoints = null;
            mBezierPoints = buildBezierPoints();
            if (mTangent) {
                mTangentPoints = buildTangentPoints();
            }
            mState &= ~STATE_READY;
            mState &= ~STATE_TOUCH;
            mState |= STATE_RUNNING;
            invalidate();
        }
    }

    /**
     * 停止
     */
    public void stop() {
        if (isRunning()) {
            mHandler.removeEvent(HANDLER_WHAT);
            mR = 0;
            mState &= ~STATE_RUNNING;
            mState &= ~STATE_STOP;
            mState |= STATE_READY | STATE_TOUCH;
            invalidate();
        }
    }

    /**
     * 添加控制点
     *
     * @return 返回添加控制点是否成功
     */
    public boolean addPoint() {
        if (isReady()) {
            int size = mControlPoints.size();
            if (size >= MAX_COUNT + 1) {
                return false;
            }
            float x = mControlPoints.get(size - 1).getPointX();
            float y = mControlPoints.get(size - 1).getPointY();
            getRegion();
            int t = 0;
            int len = mRegion.length;
            SecureRandom random;
            try {
                random = SecureRandom.getInstance("SHA1PRNG");
                while (true) { // 随机赋值
                    t++;
                    if (t > len) { // 超出region长度，跳出随机赋值
                        t = 0;
                        break;
                    }
                    int rand = random.nextInt(len);
                    float px = x + mRegion[rand][0];
                    float py = y + mRegion[rand][1];
                    if (isLegalTouchRegion(px, py)) {
                        mControlPoints.add(new Point(px, py));
                        invalidate();
                        break;
                    }
                }
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            if (t == 0) { // 超出region长度而未赋值时，循环赋值
                for (float[] floats : mRegion) {
                    float px = x + floats[0];
                    float py = y + floats[1];
                    if (isLegalTouchRegion(px, py)) {
                        mControlPoints.add(new Point(px, py));
                        invalidate();
                        break;
                    }
                }
            }
            return true;
        }
        return false;
    }

    private void getRegion() {
        if (mRegion == null) {
            int re = mWidth / 5;
            mRegion = new float[][]{
                    {0, re},
                    {0, -re},
                    {re, re},
                    {-re, -re},
                    {re, 0},
                    {-re, 0},
                    {0, 1.5f * re},
                    {0, -1.5f * re},
                    {1.5f * re, 1.5f * re},
                    {-1.5f * re, -1.5f * re},
                    {1.5f * re, 0},
                    {-1.5f * re, 0},
                    {0, 2 * re},
                    {0, -2 * re},
                    {2 * re, 2 * re},
                    {-2 * re, -2 * re},
                    {2 * re, 0},
                    {-2 * re, 0}
            };
        }
    }

    /**
     * 删除控制点
     *
     * @return 返回删除控制点是否成功
     */
    public boolean delPoint() {
        if (isReady()) {
            int size = mControlPoints.size();
            if (size <= 2) {
                return false;
            }
            mControlPoints.remove(size - 1);
            invalidate();
            return true;
        }
        return false;
    }

    /**
     * 贝塞尔曲线阶数
     *
     * @return 返回贝塞尔曲线阶数
     */
    public int getOrder() {
        return mControlPoints.size() - 1;
    }

    /**
     * 设置贝塞尔曲线阶数
     *
     * @param order 阶数
     */
    public void setOrder(int order) {
        if (getOrder() == order) {
            return;
        }
        stop();
        int size = getOrder() - order;
        if (size > 0) {
            for (int i = 0; i < size; i++) {
                delPoint();
            }
        } else {
            for (int i = -size; i > 0; i--) {
                addPoint();
            }
        }
    }

    /**
     * 贝塞尔曲线阶数
     *
     * @return 返回String类型贝塞尔曲线阶数
     */
    public String getOrderStr() {
        return ORDER_STRS[getOrder() - 1];
    }

    /**
     * 设置移动速率
     *
     * @param rate 移动速率
     */
    public void setRate(int rate) {
        mRate = rate;
    }

    /**
     * 设置是否显示切线
     *
     * @param tangent 是否显示切线
     */
    public void setTangent(boolean tangent) {
        mTangent = tangent;
    }

    /**
     * 设置是否循环
     *
     * @param loop 是否循环运动
     */
    public void setLoop(boolean loop) {
        mLoop = loop;
    }
}
